﻿ #region Disclaimer/Info
 
 /////////////////////////////////////////////////////////////////////////////////////////////////
 //
 //   File:		User.Actions.cs
 //   Website:		http://dexterblogengine.com/
 //   Authors:		http://dexterblogengine.com/About.ashx
 //   Rev:		1
 //   Created:		19/01/2011
 //   Last edit:		19/01/2011
 //   License:		GNU Library General Public License (LGPL)
 //   File:            User.Actions.cs
 //   For updated news and information please visit http://dexterblogengine.com/
 //   Dexter is hosted to Codeplex at http://dexterblogengine.codeplex.com
 //   For any question contact info@dexterblogengine.com
 //
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 #endregion
 
using System;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using Topics.Radical.Validation;

namespace Dexter.Domain.Model {
	public partial class User {
		#region Public Methods

		/// <summary>
		/// Changes the password question and answer.
		/// </summary>
		/// <param name="newPasswordQuestion">The new password question.</param>
		/// <param name="newPasswordAnswer">The new password answer.</param>
		public virtual void ChangePasswordQuestionAndAnswer ( string newPasswordQuestion , string newPasswordAnswer ) {
			PasswordQuestion = newPasswordQuestion;

			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Clear:
					PasswordAnswer = newPasswordAnswer;
					break;
				case EncryptDataFormat.Encrypted:
					PasswordAnswerSalt = GenerateSalt ( );
					PasswordAnswer = EncryptValue ( newPasswordAnswer );
					break;
				case EncryptDataFormat.Hashed:
					PasswordAnswer = CalculateHashForPassword ( newPasswordAnswer );
					break;
				default:
					throw new SecurityException ( "Password mode not supported" );
			}

			PasswordAnswer = newPasswordAnswer;
			UpdateLastActivityDate ( );
		}

		/// <summary>
		/// Changes the password of the user, changes salt each password generation.
		/// </summary>
		/// <param name="newpwd">The newpwd.</param>
		public virtual Boolean ChangePassword ( string newpwd ) {
			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Clear:
					Password = newpwd;
					break;
				case EncryptDataFormat.Hashed:
					ChangePasswordWithHash ( newpwd );
					break;
				case EncryptDataFormat.Encrypted:
					Password = EncryptValue ( newpwd );
					break;
				default:
					throw new SecurityException ( "Password mode not supported" );
			}

			UpdateLastActivityDate ( );
			LastPasswordChangeDate = DateTime.Now;
			return true;
		}

		/// <summary>
		/// Verifies the password passsed in clear form
		/// </summary>
		/// <param name="password">The newpwd.</param>
		/// <returns></returns>
		public virtual Boolean VerifyPassword ( string password ) {
			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Clear:
					return password == Password;
				case EncryptDataFormat.Hashed:
					return Password == CalculateHashForPassword ( password );
				default:
					throw new SecurityException ( "Encryption mode not supported" );
			}
		}

		public virtual Boolean VerifyPasswordAnswer ( string answer ) {
			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Clear:
					return answer == Password;
				case EncryptDataFormat.Hashed:
					return PasswordAnswer == CalculateHashForPassword ( answer );
				default:
					throw new SecurityException ( "Encryption mode not supported" );
			}
		}

		/// <summary>
		/// Verifies the password with digest.
		/// </summary>
		/// <param name="token">The token.</param>
		/// <param name="authToken">The auth token.</param>
		public virtual Boolean VerifyPasswordWithDigest ( string token , string authToken ) {
			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Hashed:

					using ( HashAlgorithm hash = HashAlgorithm.Create ( HashProvider.ToString ( ) ) ) {
						Byte[] btoken = Convert.FromBase64String ( token );
						byte[] bPassword = Convert.FromBase64String ( Password );
						var nonHashedGoodToken = new byte[btoken.Length + bPassword.Length];
						Buffer.BlockCopy ( bPassword , 0 , nonHashedGoodToken , 0 , bPassword.Length );
						Buffer.BlockCopy ( btoken , 0 , nonHashedGoodToken , bPassword.Length , btoken.Length );
						String goodToken = Convert.ToBase64String ( hash.ComputeHash ( nonHashedGoodToken ) );
						return goodToken == authToken;
					}


				default:
					throw new SecurityException ( "Password mode not supported" );
			}
		}

		public virtual string UnencodePassword ( ) {
			switch ( EncryptDataFormat ) {
				case EncryptDataFormat.Clear:
					return Password;

				case EncryptDataFormat.Encrypted:
					return Decrypt ( Password );

				default:
					throw new SecurityException ( "Cannot Retrieve Hashed" );
			}
		}

		/// <summary>
		/// Unlocks the user;
		/// </summary>
		public virtual void Unlock ( ) {
			IsLockedOut = false;
			LastLockedOutDate = DateTime.Now;
			UpdateLastActivityDate ( );
		}

		/// <summary>
		/// Locks the user;
		/// </summary>
		public virtual void Lock ( ) {
			IsLockedOut = true;
			LastLockedOutDate = DateTime.Now;
			UpdateLastActivityDate ( );
		}

		/// <summary>
		/// Approves the user;
		/// </summary>
		public virtual void Approve ( ) {
			IsApproved = true;
		}

		public virtual void Unapprove ( ) {
			IsApproved = false;
		}

		/// <summary>
		/// Determines whether the user is into the specified role..
		/// </summary>
		/// <param name="roleName">Name of the role.</param>
		/// <returns>
		/// 	<c>true</c> if the user is in role; otherwise, <c>false</c>.
		/// </returns>
		public virtual bool IsInRole ( string roleName ) {
			Ensure.That ( roleName )
				.Named ( "roleName" )
				.WithPreview ( ( v , e ) => Logger.Warn ( v.GetFullErrorMessage ( ) , e ) )
				.IsNotNullNorEmpty ( )
				.WithMessage ( "Invalid role name." );

			return Roles.Where ( x => x.LoweredName == roleName.ToLowerInvariant ( ) ).Count ( ) > 0;
		}

		public virtual void UpdateLastActivityDate ( ) {
			LastActivityDate = DateTime.Now;
		}

		#endregion

		#region Private/Internal methods

		/// <summary>
		/// Verifies the password and use cryptoagiliy to update hash if needed.
		/// The problem is, we can have the password stored with an Hash, but the
		/// global hash provider needs another kind of hash, so we need to update the
		/// object accordingly.
		/// </summary>
		/// <param name="password">Password in clear form.</param>
		/// <param name="currentGlobalHashProvider">The global Hash provider.</param>
		/// <returns></returns>
		protected internal virtual Boolean VerifyPassword ( string password , HashProvider currentGlobalHashProvider ) {
			if ( !VerifyPassword ( password ) ) {
				return false;
			}

			if ( currentGlobalHashProvider != HashProvider ) {
				HashProvider = currentGlobalHashProvider;
				ChangePassword ( password );
			}
			return true;
		}

		void ChangePasswordWithHash ( string newpwd ) {
			PasswordSalt = GenerateSalt ( );
			Password = CalculateHashForPassword ( newpwd );
		}

		protected internal virtual string GenerateSalt ( ) {
			var rnd = new RNGCryptoServiceProvider ( );
			var salt = new byte[16];
			rnd.GetBytes ( salt );
			return Convert.ToBase64String ( salt );
		}

		string CalculateHashForPassword ( string newpwd ) {
			using ( HashAlgorithm hash = HashAlgorithm.Create ( HashProvider.ToString ( ) ) ) {
				Byte[] password = Encoding.UTF8.GetBytes ( newpwd );
				Byte[] salt = Convert.FromBase64String ( PasswordSalt );
				var dst = new byte[password.Length + salt.Length];
				Buffer.BlockCopy ( password , 0 , dst , 0 , password.Length );
				Buffer.BlockCopy ( salt , 0 , dst , password.Length , salt.Length );
				return Convert.ToBase64String ( hash.ComputeHash ( dst ) );
			}
		}

		public virtual void UpdateFailurePassword ( double passwordAttemptWindow, int maxInvalidPasswordAttempts ) {
			DateTime now = FailedPasswordAttemptWindowStart;
			DateTime time2 = now.AddMinutes ( passwordAttemptWindow );
			int failedPasswordAttemptCount = FailedPasswordAttemptCount;
			if ( ( failedPasswordAttemptCount == 0 ) || ( DateTime.Now > time2 ) ) {
				FailedPasswordAttemptWindowStart = DateTime.Now;
				FailedPasswordAttemptCount = 1;
			}
			else {
				failedPasswordAttemptCount++;
				if ( failedPasswordAttemptCount >= maxInvalidPasswordAttempts ) {
					Lock ( );
				}
				else {
					FailedPasswordAttemptCount = failedPasswordAttemptCount;
				}
			}
		}

		public virtual void UpdateFailurePasswordAswer ( double passwordAnswerAttemptWindow, int maxInvalidPasswordAnswerAttempts ) {
			DateTime now = FailedPasswordAnswerAttemptWindowStart;
			DateTime time2 = now.AddMinutes ( passwordAnswerAttemptWindow );
			int failedPasswordAnswerAttemptCount = FailedPasswordAnswerAttemptCount;

			if ( ( failedPasswordAnswerAttemptCount == 0 ) || ( DateTime.Now > time2 ) ) {
				FailedPasswordAnswerAttemptWindowStart = DateTime.Now;
				FailedPasswordAnswerAttemptCount = 1;
			}
			else {
				failedPasswordAnswerAttemptCount++;
				if ( failedPasswordAnswerAttemptCount >= maxInvalidPasswordAnswerAttempts ) {
					Lock ( );
				}
				else {
					FailedPasswordAnswerAttemptCount = failedPasswordAnswerAttemptCount;
				}
			}
		}

		static string EncryptValue ( string password ) {
			return password.Encrypt ( "{0x68a04911,0x312b,0x4d3c,{0xac,0x71,0x5d,0x80,0xea,0xae,0x11,0x23}}" );
		}

		static string Decrypt ( string encryptedData ) {
			return encryptedData.Decrypt ( "{0x68a04911,0x312b,0x4d3c,{0xac,0x71,0x5d,0x80,0xea,0xae,0x11,0x23}}" );
		}

		#endregion
	}
}
